﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Configuration.Install;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.ServiceProcess;
using System.Threading;

namespace EventLogMailer
{
    public abstract class ServiceApplicationBase
    {
        private readonly TraceSource trace;
        
        private const string InstallServiceArgumentName = "install";
        private const string UninstallServiceArgumentName = "uninstall";
        private const string DebugArgumentName = "debug";

        protected TraceSource Trace
        {
            get { return this.trace; }
        }
        
        protected ServiceApplicationBase(TraceSource trace)
        {
            if (trace == null) throw new ArgumentNullException("trace");

            this.trace = trace;
        }

        protected bool ServiceMode
        {
            get { return Environment.UserInteractive == false; }
        }

        public void Run(string[] args)
        {
            try
            {
                var command = args.FirstOrDefault() ?? string.Empty;


                if (string.IsNullOrEmpty(command) || command.Equals(DebugArgumentName, StringComparison.InvariantCultureIgnoreCase))
                {
                    if (this.ServiceMode)
                    {
                        var assemblyLocation = Assembly.GetEntryAssembly().Location;
                        var assemblyDirectory = Path.GetDirectoryName(assemblyLocation);

                        if (assemblyDirectory != null) Environment.CurrentDirectory = assemblyDirectory;

                        this.trace.TraceEvent(TraceEventType.Information, 16, "StartingServerInServiceMode");

                        var service = new Service(this);
                        ServiceBase.Run(new ServiceBase[] { service });
                    }
                    else
                    {
                        this.trace.TraceEvent(TraceEventType.Information, 15, "StartingServerInConsoleMode");
                        this.StartInConsoleMode(args);
                    }
                }

                else if (String.Equals(command, InstallServiceArgumentName, StringComparison.InvariantCultureIgnoreCase))
                {
                    this.InstallService();
                }

                else if (String.Equals(command, UninstallServiceArgumentName,
                                       StringComparison.InvariantCultureIgnoreCase))
                {
                    this.UninstallService();
                }
                else
                {
                    var message = string.Format("Unrecognized command: {0}. Use: {1}, {2} or nothing to start service/console.", command, InstallServiceArgumentName, UninstallServiceArgumentName);
                    throw new InvalidOperationException(message);
                }
            }
            catch (Exception ex)
            {
                this.trace.TraceEvent(TraceEventType.Critical, 17, "ServerConfigurationLoadingException", ex);
                Environment.ExitCode = 1;
            }
        }

        private void UninstallService()
        {
            var installer = this.BuildInstaller();
            // ReSharper disable AssignNullToNotNullAttribute
            installer.Uninstall(null);
            // ReSharper restore AssignNullToNotNullAttribute
        }

        /// <summary>
        ///   Start the server in the console mode.
        /// </summary>
        /// <param name = "args">command-line arguments</param>
        private void StartInConsoleMode(string[] args)
        {
            this.StartServerInternal(args);

            Console.ReadKey();

            this.StopServerInternal();
            this.trace.TraceEvent(TraceEventType.Information, 14, "ServerStopped");
        }

        private void StartServerInternal(string[] args)
        {
            var command = args.FirstOrDefault() ?? string.Empty;
            var isDebugMode = command.Equals(DebugArgumentName, StringComparison.InvariantCultureIgnoreCase);

            if (isDebugMode)
            {
                Debugger.Launch();
            }
            

            try
            {
                this.IntializeCustomComponents(args);
                
                this.StartServer(args);
            }
            catch (Exception e)
            {
                this.trace.TraceEvent(TraceEventType.Critical, 0, "Failed to start server: " + e);
                throw;
            }
        }

   

        private void StopServerInternal()
        {
            try
            {
                this.StopServer();
            }
            catch (Exception e)
            {
                this.trace.TraceEvent(TraceEventType.Critical, 0, "Failed to stop server: " + e);
                throw;
            }
        }

        protected abstract void StopServer();

        protected abstract void StartServer(string[] args);

        protected virtual void IntializeCustomComponents(string[] args) { }

        private void InstallService()
        {
            var installer = this.BuildInstaller();
            var savedState = new Hashtable();
            installer.Install(savedState);
        }

        private TransactedInstaller BuildInstaller()
        {
            var assemblyPath = Assembly.GetEntryAssembly().Location;

            var installContext = new InstallContext(this.GetType().Name + ".InstallLog", new string[0]);
            installContext.Parameters["assemblypath"] = assemblyPath;

            TransactedInstaller installer = new TransactedInstaller
            {
                Context = installContext
            };

            var installers = this.CreateServiceInstallers();
            installer.Installers.AddRange(installers.ToArray());


            return installer;
        }

        

        protected virtual IEnumerable<Installer> CreateServiceInstallers()
        {
            ServiceProcessInstaller serviceProcessInstaller = new ServiceProcessInstaller
            {
                Account = ServiceAccount.NetworkService,
                Password = null,
                Username = null,
            };


            var serviceInstaller = this.CreateServiceInstaller();

            var installers = new Installer[] { serviceProcessInstaller, serviceInstaller };
            return installers;
        }

        protected abstract ServiceInstaller CreateServiceInstaller();

        #region Nested type: Service

        private class Service : ServiceBase
        {
            private readonly ServiceApplicationBase serviceApplication;
            private readonly object waitForServiceStopSyncRoot = new object();

            public Service(ServiceApplicationBase serviceApplication)
            {
                this.serviceApplication = serviceApplication;
            }

            protected override void OnStop()
            {
                this.serviceApplication.StopServerInternal();
                this.serviceApplication.trace.TraceEvent(TraceEventType.Information, 1, "ServerStopped");

                Monitor.Enter(this.waitForServiceStopSyncRoot);
                Monitor.PulseAll(this.waitForServiceStopSyncRoot);
                Monitor.Exit(this.waitForServiceStopSyncRoot);
            }

            protected override void OnStart(string[] args)
            {
                this.serviceApplication.StartServerInternal(args);

                Thread waitForServiceStopThread = new Thread(() =>
                {
                    //wait for stop signal 
                    Monitor.Enter(this.waitForServiceStopSyncRoot);
                    Monitor.Wait(this.waitForServiceStopSyncRoot);
                    Monitor.Exit(this.waitForServiceStopSyncRoot);
                })
                {
                    IsBackground = false,
                    Name = "WaitForServiceStopThread"
                };
                waitForServiceStopThread.Start();
            }
        }

        #endregion
    }
}